Add delbin support from Paul Cornett.
authorrobertl <robertl>
Wed, 24 Jun 2009 03:24:23 +0000 (03:24 +0000)
committerrobertl <robertl>
Wed, 24 Jun 2009 03:24:23 +0000 (03:24 +0000)
Makefile.in
delbin.c [new file with mode: 0644]
vecs.c

index 855369caa9e2f3b17e230bf1478b2ac0090eb50c..f905aacba0bfd30629376b1356502e334d3f240f 100644 (file)
@@ -62,7 +62,7 @@ ALL_FMTS=$(MINIMAL_FMTS) gtm.o gpsutil.o pcx.o cetus.o copilot.o \
        navilink.o mtk_logger.o ik3d.o osm.o destinator.o exif.o vidaone.o \
        igo8.o gopal.o humminbird.o mapasia.o gnav_trl.o navitel.o ggv_ovl.o \
        jtr.o sbp.o sbn.o mmo.o skyforce.o itracku.o v900.o \
-       pocketfms_bc.o pocketfms_fp.o naviguide.o
+       pocketfms_bc.o pocketfms_fp.o naviguide.o delbin.o
 
 FMTS=@FMTS@
 
diff --git a/delbin.c b/delbin.c
new file mode 100644 (file)
index 0000000..0fdc6e4
--- /dev/null
+++ b/delbin.c
@@ -0,0 +1,2748 @@
+/*
+       DeLorme PN-20/40 USB "DeLBin" protocol
+
+    Copyright (C) 2009 Paul Cornett, pc-gpsb at bullseye.com
+    Copyright (C) 2005  Robert Lipe, robertlipe@usa.net
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 USA
+
+ */
+
+#include "defs.h"
+#include <assert.h>
+
+#define MYNAME "delbin"
+
+/*
+Device documentation:
+"DeLorme Binary GPS Format", delbin_user_interface_format_176.pdf
+obtained here: http://forum.delorme.com/viewtopic.php?t=13846
+
+Notes:
+Initial development was done with a PN-40, firmware 2.4.123299. The test
+device was upgraded to firmware 2.5.165506 during development.
+
+The "data size" in the message header includes the 4 trailer bytes, so it
+is really the size of the whole message minus the header.
+
+The time before an unacknowledged message will be retransmitted by the
+device is on the order of 2 to 4 seconds.
+
+Retrieving all tracks at once (using code 0 in message 0xb031) does not
+seem to work, it hangs after the first track, maybe waiting for some
+undocumented response message.
+
+Character encoding is not documented, appears to be 8859-1.
+
+The undocumented messages 0xaa01, 0xb015, 0xb016 and the use of the
+"reserved" byte in message 0xb012 were discovered by examining the data
+transferred between the device and DeLorme Topo 8.0. They may have been
+added in the PN-40 2.5 firmware.
+*/
+
+//-----------------------------------------------------------------------------
+// interface to platform-specific device I/O
+typedef struct {
+       void (*init)(const char* name);
+       void (*deinit)(void);
+       unsigned (*packet_read)(void*);
+       unsigned (*packet_write)(const void*, unsigned);
+} delbin_os_ops_t;
+
+// really static, only extern so it can be forward declared
+extern delbin_os_ops_t delbin_os_ops;
+
+static unsigned delbin_os_packet_size;
+//-----------------------------------------------------------------------------
+
+// number of times to attempt a transfer before giving up
+#define ATTEMPT_MAX 2
+
+// debug output: low, medium, high
+#define DBGLVL_L 1
+#define DBGLVL_M 2
+#define DBGLVL_H 3
+
+#define UNKNOWN_ELEV -2000000
+
+#define sizeofarray(x) (sizeof(x) / sizeof(x[0]))
+
+static char* opt_getposn;
+static char* opt_logs;
+static char* opt_long_notes;
+
+static arglist_t delbin_args[] = {
+       { "get_posn", &opt_getposn, "Return current position as a waypoint",
+               NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
+       { "logs", &opt_logs, "Include groundspeak logs when writing",
+               NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
+       { "long_notes", &opt_long_notes, "Use long waypoint notes regardless of PN version",
+               NULL, ARGTYPE_BOOL, ARG_NOMINMAX },
+       ARG_TERMINATOR
+};
+
+// Whether device understands message 0xb016
+static int use_extended_notes;
+
+static const char* waypoint_symbol(unsigned index);
+static unsigned waypoint_symbol_index(const char* name);
+static int track_color(unsigned index);
+static unsigned track_color_index(int bgr);
+
+static unsigned waypoint_i;
+static unsigned waypoint_n;
+static waypoint** wp_array;
+
+//-----------------------------------------------------------------------------
+// Message ids and sizes. Only the needed ones are here.
+// Note that "in" and "out" ids are named as in the device documentation,
+// so "in" means to the device, "out" means from.
+#define MSG_ACK 0xaa00
+#define MSG_BREAK 0xaa02
+#define MSG_BREAK_SIZE 33
+#define MSG_NAVIGATION 0xa010
+#define MSG_REQUEST_ROUTES 0xb051
+#define MSG_REQUEST_ROUTES_SIZE 65
+#define MSG_REQUEST_TRACKS 0xb031
+#define MSG_REQUEST_TRACKS_SIZE 33
+#define MSG_REQUEST_WAYPOINTS 0xb012
+#define MSG_REQUEST_WAYPOINTS_SIZE 15
+#define MSG_ROUTE_COUNT 0xb050
+#define MSG_ROUTE_HEADER_IN 0xb055
+#define MSG_ROUTE_HEADER_OUT 0xb052
+#define MSG_ROUTE_POINT_IN 0xb056
+#define MSG_ROUTE_POINT_OUT 0xb053
+#define MSG_ROUTE_SHAPE_IN 0xb057
+#define MSG_ROUTE_SHAPE_OUT 0xb054
+#define MSG_SATELLITE_INFO 0xa020
+#define MSG_TRACK_COUNT 0xb030
+#define MSG_TRACK_HEADER_IN 0xb035
+#define MSG_TRACK_HEADER_OUT 0xb032
+#define MSG_TRACK_POINT_IN 0xb036
+#define MSG_TRACK_POINT_OUT 0xb033
+#define MSG_TRANSFER_COMPLETE 0xaa04
+#define MSG_VERSION 0xa001
+#define MSG_WAYPOINT_COUNT 0xb010
+#define MSG_WAYPOINT_IN 0xb014
+#define MSG_WAYPOINT_OUT 0xb013
+// Undocumented:
+// This one looks like MSG_ACK, except it also has a string in it that says
+// something like "device is busy". The expected MSG_ACK usually immediately
+// follows it, so the point of this one is unclear.
+#define MSG_NACK 0xaa01
+// Long waypoint notes
+#define MSG_WAYPOINT_NOTE_IN 0xb016
+#define MSG_WAYPOINT_NOTE_OUT 0xb015
+
+//-----------------------------------------------------------------------------
+// Message structures
+
+// Output Waypoint Message
+// Message ID: 0xB013
+// Input Waypoint Message
+// Message ID: 0xB014
+typedef struct {
+       gbuint8 total[4]; // U32
+       gbuint8 index[4]; // U32
+       gbuint8 year;
+       gbuint8 month;
+       gbuint8 day;
+       gbuint8 hour;
+       gbuint8 minute;
+       gbuint8 second;
+       gbuint8 latitude[4]; // S32 rad * 100000000
+       gbuint8 longitude[4]; // S32 rad * 100000000
+       gbuint8 elevation[4]; // F32 meters
+       gbuint8 color;
+       gbuint8 symbol;
+       gbuint8 name_size;
+       char name[1];
+       // note_size[2] U16
+       // note[note_size]
+} msg_waypoint_t;
+
+// undocumented, seen with PN-40 2.5 firmware
+// output waypoint note
+// Message ID: 0xB015
+// input waypoint note
+// Message ID: 0xB016
+typedef struct {
+       gbuint8 index[2];
+       gbuint8 total[2];
+       gbuint8 name_size;
+       char name[1];
+       // note_size[2]
+       // note[note_size]
+} msg_waypoint_note_t;
+
+// Output Track Point Message
+// Message ID: 0xB033
+// Input Track Point Message
+// Message ID: 0xB036
+typedef struct {
+       gbuint8 total[4]; // U32
+       gbuint8 index[4]; // U32
+       gbuint8 number;
+       struct {
+               gbuint8 year;
+               gbuint8 month;
+               gbuint8 day;
+               gbuint8 hour;
+               gbuint8 minute;
+               gbuint8 second;
+               gbuint8 latitude[4]; // S32 rad * 100000000
+               gbuint8 longitude[4]; // S32 rad * 100000000
+               gbuint8 elevation[4]; // F32 meters
+               gbuint8 speed[2]; // U16 km/h * 10
+               gbuint8 heading[2]; // U16 deg * 100
+               gbuint8 status;
+       } point[1];
+} msg_track_point_t;
+
+// Output Track Header (Name) Message
+// Message ID: 0xB032
+typedef struct {
+       gbuint8 total_tracks[2]; // U16
+       gbuint8 number[2]; // U16
+       char name[32];
+       gbuint8 total_points[4]; // U32
+       gbuint8 year;
+       gbuint8 month;
+       gbuint8 day;
+       gbuint8 hour;
+       gbuint8 minute;
+       gbuint8 second;
+       gbuint8 color[2]; // U16
+       gbuint8 distance[4]; // U32 m
+       gbuint8 duration[4]; // U32 sec
+       gbuint8 comment_size[2]; // U16
+       char comment[1];
+} msg_track_header_t;
+
+// Input Upload Track Header Message
+// Message ID: 0xB035
+typedef struct {
+       char name[32];
+       gbuint8 total_points[4]; // U32
+       gbuint8 year;
+       gbuint8 month;
+       gbuint8 day;
+       gbuint8 hour;
+       gbuint8 minute;
+       gbuint8 second;
+       gbuint8 color[2]; // U16
+       gbuint8 comment_size[2]; // U16
+       char comment[1];
+} msg_track_header_in_t;
+
+// Output Route Shape Message
+// Message ID: 0xB054
+typedef struct {
+       gbuint8 total[4]; // U32
+       gbuint8 index[4]; // U32
+       gbuint8 number;
+       gbuint8 reserved;
+       struct {
+               gbuint8 latitude[4]; // S32 rad * 100000000
+               gbuint8 longitude[4]; // S32 rad * 100000000
+       } point[1];
+} msg_route_shape_t;
+
+// Output Route Point Message
+// Message ID: 0xB053
+// Input Route Itin Point Message
+// Message ID: 0xB056
+typedef struct {
+       gbuint8 total[4]; // U32
+       gbuint8 index[4]; // U32
+       char name[32];
+       gbuint8 latitude[4]; // S32 rad * 100000000
+       gbuint8 longitude[4]; // S32 rad * 100000000
+       gbuint8 time_from_start[4]; // U32 sec
+       gbuint8 distance_from_start[4]; // F32 km
+       gbuint8 bearing_in[2]; // U16 deg * 100
+       gbuint8 bearing_out[2]; // U16 deg * 100
+       gbuint8 bearing_next[2]; // U16 deg * 100
+       gbuint8 itinerary_type;
+       gbuint8 turn_type;
+       gbuint8 road_class[2]; // U16
+       gbuint8 feature_code[4]; // U32
+       gbuint8 exit_label_size;
+       char exit_label[1];
+       // comment_size U8
+       // comment[comment_size]
+       // shape_pt_count U32
+} msg_route_point_t;
+
+// Output Route Header (Name) Message
+// Message ID: 0xB052
+typedef struct {
+       gbuint8 total[2]; // U16
+       gbuint8 index[2]; // U16
+       char name[64];
+       gbuint8 type;
+       gbuint8 total_route_point[4]; // U32
+       gbuint8 total_shape_point[4]; // U32
+} msg_route_header_t;
+
+// Input Upload Route Header Message
+// Message ID: 0xB055
+typedef struct {
+       char name[64];
+       gbuint8 type;
+       gbuint8 total_route_point[4]; // U32
+       gbuint8 total_shape_point[4]; // U32
+} msg_route_header_in_t;
+
+// Output Navigation Message
+// Message ID: 0xA010
+typedef struct {
+       gbuint8 gps_week[2]; // U16
+       gbuint8 time_of_week[8]; // D64 sec
+       gbuint8 year[2]; // U16
+       gbuint8 month;
+       gbuint8 day;
+       gbuint8 hour;
+       gbuint8 minute;
+       gbuint8 second;
+       gbuint8 satellites;
+       gbuint8 latitude[8]; // D64 deg
+       gbuint8 longitude[8]; // D64 deg
+       gbuint8 elevation[8]; // D64 meters
+       gbuint8 geoid_offset[2]; // S16 meters * 10
+       gbuint8 speed[4]; // F32 km/h
+       gbuint8 heading[2]; // U16 deg * 100
+       gbuint8 magnetic_variation[2]; // S16 deg * 100
+       gbuint8 fix_status;
+} msg_navigation_t;
+
+// Output Satellite Info Message
+// Message ID: 0xA020
+typedef struct {
+       gbuint8 gps_week[2]; // U16
+       gbuint8 time_of_week[8]; // D64 sec
+       gbuint8 hdop[2]; // U16
+       gbuint8 vdop[2]; // U16
+       gbuint8 pdop[2]; // U16
+       gbuint8 number;
+       struct {
+               gbuint8 prn;
+               gbuint8 azimuth[2]; // S16 deg? * 100
+               gbuint8 elevation[2]; // S16 deg? * 100
+               gbuint8 Cn0[2]; // U16 snr * 100
+               gbuint8 status;
+       } sat[1];
+} msg_satellite_t;
+
+// Output Version Message
+// Message ID: 0xA001
+typedef struct {
+       gbuint8 firmware_version[4];
+       char company[32];
+       char product[32];
+       char firmware[32];
+       char gps_firmware[48];
+       char serial[16];
+       char extra[16];
+} msg_version_t;
+
+//-----------------------------------------------------------------------------
+
+static gbuint16
+checksum(const gbuint8* p, unsigned n)
+{
+       int x = 0;
+       unsigned i;
+       for (i = n / 2; i > 0; i--) {
+               x += *p++;
+               x += *p++ << 8;
+       }
+       if (n & 1) {
+               x += *p;
+       }
+       return (gbuint16)-x;
+}
+
+//-----------------------------------------------------------------------------
+// OS packet read/write wrappers
+
+static unsigned
+packet_read(void* buf)
+{
+       unsigned n = delbin_os_ops.packet_read(buf);
+       if (n == 0) {
+               fatal(MYNAME ": read 0\n");
+       }
+       if (global_opts.debug_level >= DBGLVL_H) {
+               unsigned j;
+               warning(MYNAME ": pcktrd ");
+               for (j = 0; j < n; j++) {
+                       warning("%02x ", ((gbuint8*)buf)[j]);
+               }
+               warning("\n");
+       }
+       return n;
+}
+
+static void
+packet_write(const void* p, unsigned size)
+{
+       unsigned n;
+       if (global_opts.debug_level >= DBGLVL_H) {
+               unsigned j;
+               warning(MYNAME ": pcktwr ");
+               for (j = 0; j < size; j++) {
+                       warning("%02x ", ((gbuint8*)p)[j]);
+               }
+               warning("\n");
+       }
+       n = delbin_os_ops.packet_write(p, size);
+       if (n != size) {
+               fatal(MYNAME ": short write %u %u\n", size, n);
+       }
+}
+
+//-----------------------------------------------------------------------------
+
+// dynamically sized buffer with space reserved for message header and trailer
+typedef struct {
+       // message data size
+       unsigned size;
+       // buffer size
+       unsigned capacity;
+       gbuint8* buf;
+       // convenience pointer to message data area
+       void* data;
+} message_t;
+
+static void
+message_init(message_t* m)
+{
+       m->capacity = 100;
+       m->buf = xmalloc(m->capacity);
+       m->data = m->buf + 2 + 8;
+}
+
+static void
+message_init_size(message_t* m, unsigned size)
+{
+       m->size = size;
+       m->capacity = 2 + 8 + size + 4;
+       m->buf = xmalloc(m->capacity);
+       m->data = m->buf + 2 + 8;
+}
+
+static void
+message_free(message_t* m)
+{
+       xfree(m->buf);
+       m->buf = NULL;
+       m->data = NULL;
+}
+
+static void
+message_ensure_size(message_t* m, unsigned size)
+{
+       m->size = size;
+       if (m->capacity < 2 + 8 + size + 4) {
+               m->capacity = 2 + 8 + size + 4;
+               xfree(m->buf);
+               m->buf = xmalloc(m->capacity);
+               m->data = m->buf + 2 + 8;
+       }
+}
+
+static unsigned
+message_get_id(const message_t* m)
+{
+       return le_readu16(m->buf + 4);
+}
+
+//-----------------------------------------------------------------------------
+
+static void
+message_write(unsigned msg_id, message_t* m)
+{
+       unsigned chksum;
+       unsigned count;
+       unsigned n;
+       gbuint8* p = m->buf;
+
+       // header (2 start bytes filled in later)
+       p[2] = 0xdb;
+       p[3] = 0xfe;
+       le_write16(p + 4, msg_id);
+       // "data size" includes 4 trailer bytes
+       le_write16(p + 6, m->size + 4);
+       chksum = checksum(p + 2, 6);
+       le_write16(p + 8, chksum);
+       // message data (filled in by caller)
+       chksum = checksum(m->data, m->size);
+       n = 2 + 8 + m->size;
+       // trailer (checksum and marker bytes)
+       le_write16(p + n, chksum);
+       p[n + 2] = 0xad;
+       p[n + 3] = 0xbc;
+       // size of message not counting packet start bytes
+       count = 8 + m->size + 4;
+       do {
+               const gbuint8 save0 = p[0];
+               const gbuint8 save1 = p[1];
+               n = delbin_os_packet_size - 2;
+               if (n > count) {
+                       n = count;
+               }
+               // doc. says 0x20, device sends 0, probably ignored
+               p[0] = 0x20;
+               // valid bytes in packet after first 2
+               p[1] = n;
+               packet_write(p, 2 + n);
+               p[0] = save0;
+               p[1] = save1;
+               p += n;
+               count -= n;
+       } while (count != 0);
+       if (global_opts.debug_level >= DBGLVL_M)
+               warning(MYNAME ": sent %x\n", msg_id);
+}
+
+// Get one valid message.
+// If a corrupted message with the right id is seen, return failure (0).
+static unsigned
+message_read_1(unsigned msg_id, message_t* m)
+{
+       gbuint8 buf[256];
+       gbuint8* p;
+       unsigned total = 0;
+       unsigned count = 0;
+       unsigned id = 0;
+
+       for (;;) {
+               for (;;) {
+                       unsigned n = packet_read(buf);
+                       if (n >= 10 && buf[2] == 0xdb && buf[3] == 0xfe &&
+                           checksum(buf + 2, 6) == le_readu16(buf + 8))
+                       {
+                               count = buf[1] - 8;
+                               total = le_readu16(buf + 6);
+                               id = le_readu16(buf + 4);
+                               message_ensure_size(m, total - 4);
+                               memcpy(m->buf, buf, 2 + buf[1]);
+                               break;
+                       }
+               }
+               while (count < total && buf[1] == delbin_os_packet_size - 2) {
+                       unsigned n;
+                       packet_read(buf);
+                       n = buf[1];
+                       if (n > total - count) {
+                               n = total - count;
+                       }
+                       memcpy((char*)m->data + count, buf + 2, n);
+                       count += n;
+               }
+               p = (gbuint8*)m->data + m->size;
+               if (checksum(m->data, m->size) == le_readu16(p) &&
+                       p[2] == 0xad && p[3] == 0xbc)
+               {
+                       if (global_opts.debug_level >= DBGLVL_M)
+                               warning(MYNAME ": received %x\n", id);
+                       break;
+               }
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": corrupted message %x\n", id);
+               if (id == msg_id) {
+                       id = 0;
+                       break;
+               }
+       }
+       return id;
+}
+
+// Send MSG_ACK for given message
+static void
+message_ack(unsigned id, const message_t* m)
+{
+       message_t ack;
+       char* p1;
+       const char* p2 = m->data;
+       switch (id) {
+       case MSG_ACK:
+       case MSG_NACK:
+       case MSG_NAVIGATION:
+       case MSG_SATELLITE_INFO:
+               // don't ack these
+               return;
+       }
+       message_init_size(&ack, 4);
+       p1 = ack.data;
+       // ack payload is id and body checksum of acked message
+       le_write16(p1, id);
+       p1[2] = p2[m->size];
+       p1[3] = p2[m->size + 1];
+       message_write(MSG_ACK, &ack);
+       message_free(&ack);
+}
+
+// Get specific message, ignoring others. Sends ACK for non-interval messages.
+// Gives up if 6 navigation messages are received, which means we waited at least
+// 5 seconds.
+static int
+message_read(unsigned msg_id, message_t* m)
+{
+       unsigned id;
+       int interval_message_count = 0;
+
+       if (global_opts.debug_level >= DBGLVL_M)
+               warning(MYNAME ": looking for %x\n", msg_id);
+       for (;;) {
+               id = message_read_1(msg_id, m);
+               if (id == 0) {
+                       break;
+               }
+               message_ack(id, m);
+               if (id == msg_id) {
+                       break;
+               }
+               if (id == MSG_NAVIGATION) {
+                       interval_message_count++;
+                       if (interval_message_count == 6) {
+                               break;
+                       }
+               }
+       }
+       return id == msg_id;
+}
+
+// Read a sequence of messages, up to a MSG_TRANSFER_COMPLETE
+static int
+get_batch(message_t** array, unsigned* n)
+{
+       int success = 1;
+       unsigned array_max = 100;
+       message_t* a = xmalloc(array_max * sizeof(message_t));
+       unsigned i = 0;
+       unsigned id;
+       if (global_opts.debug_level >= DBGLVL_M)
+               warning(MYNAME ": begin get_batch\n");
+       do {
+               unsigned timeout_count = 0;
+               if (i == array_max) {
+                       message_t* old_a = a;
+                       array_max += array_max;
+                       a = xmalloc(array_max * sizeof(message_t));
+                       memcpy(a, old_a, i * sizeof(message_t));
+                       xfree(old_a);
+               }
+               message_init(&a[i]);
+               for (;;) {
+                       id = message_read_1(0, &a[i]);
+                       switch (id) {
+                       case MSG_NAVIGATION:
+                               timeout_count++;
+                               if (timeout_count == 6) {
+                                       success = 0;
+                                       break;
+                               }
+                               // fall through
+                       case MSG_ACK:
+                       case MSG_NACK:
+                       case MSG_SATELLITE_INFO:
+                               continue;
+                       }
+                       break;
+               }
+               message_ack(id, &a[i]);
+               i++;
+       } while (success && id != MSG_TRANSFER_COMPLETE);
+       if (success) {
+               *array = a;
+               *n = i - 1;
+               message_free(&a[*n]);
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": end get_batch, %u messages\n", *n);
+       } else {
+               while (i--) {
+                       message_free(&a[i]);
+               }
+               xfree(a);
+               *array = NULL;
+               *n = 0;
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": end get_batch, failed\n");
+       }
+       return success;
+}
+
+static struct {
+       unsigned msg_id;
+       message_t msg;
+} *batch_array;
+static unsigned batch_array_max;
+static unsigned batch_array_i;
+
+// add a message to sequence that will later be sent all at once
+static void
+add_to_batch(unsigned id, message_t* m)
+{
+       if (batch_array_i == batch_array_max) {
+               void* old = batch_array;
+               if (batch_array_max == 0) {
+                       batch_array_max = 50;
+               }
+               batch_array_max += batch_array_max;
+               batch_array = xmalloc(batch_array_max * sizeof(*batch_array));
+               if (batch_array_i) {
+                       memcpy(batch_array, old, batch_array_i * sizeof(*batch_array));
+                       xfree(old);
+               }
+       }
+       batch_array[batch_array_i].msg_id = id;
+       batch_array[batch_array_i].msg = *m;
+       batch_array_i++;
+       memset(m, 0, sizeof(*m));
+}
+
+// send an accumulated sequence of messages
+static void
+send_batch(int expect_transfer_complete)
+{
+       message_t m;
+       const unsigned n = batch_array_i;
+       unsigned i;
+
+       message_init(&m);
+       if (global_opts.debug_level >= DBGLVL_M)
+               warning(MYNAME ": begin send_batch, %u messages\n", n);
+       for (i = 0; i < n; i++) {
+               unsigned timeout_count = 0;
+               int nack = 0;
+               message_write(batch_array[i].msg_id, &batch_array[i].msg);
+               for (;;) {
+                       unsigned id = message_read_1(0, &m);
+                       switch (id) {
+                       case MSG_ACK:
+                               if (nack) gb_sleep(100000);
+                               break;
+                       case MSG_NAVIGATION:
+                               timeout_count++;
+                               if (timeout_count > 2) {
+                                       fatal(MYNAME ": send_batch timed out\n");
+                               }
+                               if (timeout_count == 2) {
+                                       if (global_opts.debug_level >= DBGLVL_M)
+                                               warning(MYNAME ": re-sending %x\n", batch_array[i].msg_id);
+                                       message_write(batch_array[i].msg_id, &batch_array[i].msg);
+                               }
+                               // fall through
+                       case MSG_NACK:
+                       case MSG_SATELLITE_INFO:
+                               continue;
+                       default:
+                               warning(MYNAME ": unexpected response message %x during send_batch\n", id);
+                               continue;
+                       }
+                       break;
+               }
+       }
+       if (expect_transfer_complete) {
+               message_read(MSG_TRANSFER_COMPLETE, &m);
+       }
+       if (global_opts.debug_level >= DBGLVL_M)
+               warning(MYNAME ": end send_batch\n");
+       for (i = n; i--;) {
+               message_free(&batch_array[i].msg);
+       }
+       xfree(batch_array);
+       message_free(&m);
+       batch_array_i = batch_array_max = 0;
+}
+
+//-----------------------------------------------------------------------------
+// Coordinate conversion
+
+static double
+delbin_rad2deg(gbint32 x)
+{
+       return x * ((180 / M_PI) / 100000000);
+}
+
+static gbint32
+delbin_deg2rad(double x)
+{
+       return (gbint32)(x * ((M_PI / 180) * 100000000));
+}
+
+//-----------------------------------------------------------------------------
+// Waypoint reading
+
+static time_t
+decode_time(const gbuint8* p)
+{
+       struct tm t;
+       t.tm_year = p[0];
+       t.tm_mon  = p[1] - 1;
+       t.tm_mday = p[2];
+       t.tm_hour = p[3];
+       t.tm_min  = p[4];
+       t.tm_sec  = p[5];
+       return mkgmtime(&t);
+}
+
+static waypoint*
+decode_waypoint(const void* data)
+{
+       waypoint* wp = waypt_new();
+       const msg_waypoint_t* p = data;
+       const char* s;
+       float f;
+
+       wp->creation_time = decode_time(&p->year);
+       wp->latitude = delbin_rad2deg(le_read32(p->latitude));
+       wp->longitude = delbin_rad2deg(le_read32(p->longitude));
+       f = le_read_float(p->elevation);
+       if (f > UNKNOWN_ELEV) {
+               wp->altitude = f;
+       }
+       wp->icon_descr = waypoint_symbol(p->symbol);
+       if (wp->icon_descr) {
+               wp->icon_descr = xstrdup(wp->icon_descr);
+       }
+       if (p->name_size && p->name[0]) {
+               wp->description = xstrdup(p->name);
+       }
+       s = p->name + p->name_size;
+       if (le_readu16(s) &&  s[2]) {
+               wp->notes = xstrdup(s + 2);
+       }
+       return wp;
+}
+
+static void
+read_waypoints(void)
+{
+       message_t m;
+       message_t* msg_array;
+       unsigned msg_array_n;
+       waypoint* wp = NULL;
+       unsigned n_point;
+       unsigned notes_i = 0;
+       unsigned notes_max = 0;
+       unsigned i;
+       int attempt = ATTEMPT_MAX;
+
+       message_init(&m);
+       // get number of waypoints
+       for (;;) {
+               m.size = 0;
+               message_write(MSG_WAYPOINT_COUNT, &m);
+               if (message_read(MSG_WAYPOINT_COUNT, &m))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading waypoint count failed\n");
+       }
+       n_point = le_readu32(m.data);
+       if (global_opts.debug_level >= DBGLVL_L)
+               warning(MYNAME ": %u waypoints\n", n_point);
+       if (n_point == 0) {
+               message_free(&m);
+               return;
+       }
+       // get waypoint messages
+       attempt = ATTEMPT_MAX;
+       for (;;) {
+               m.size = MSG_REQUEST_WAYPOINTS_SIZE;
+               memset(m.data, 0, m.size);
+               // This byte is documented as reserved. Setting it to 3 is required to get
+               // extended notes (message 0xb015) with PN-40 firmware 2.5.
+               // Whether it has any effect with earlier firmware or the PN-20 is unknown.
+               ((char*)m.data)[1] = 3;
+               message_write(MSG_REQUEST_WAYPOINTS, &m);
+               if (get_batch(&msg_array, &msg_array_n))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading waypoints failed\n");
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": timed out reading waypoints, retrying\n");
+               m.size = MSG_BREAK_SIZE;
+               memset(m.data, 0, m.size);
+               message_write(MSG_BREAK, &m);
+       }
+       message_free(&m);
+       // process waypoint messages
+       for (i = 0; i < msg_array_n; i++) {
+               unsigned id = message_get_id(&msg_array[i]);
+               if (id == MSG_WAYPOINT_OUT) {
+                       wp = decode_waypoint(msg_array[i].data);
+                       waypt_add(wp);
+                       notes_i = 0;
+                       notes_max = 0;
+                       if (global_opts.debug_level >= DBGLVL_L)
+                               warning(MYNAME ": read waypoint '%s'\n", wp->description);
+               } else if (wp && id == MSG_WAYPOINT_NOTE_OUT) {
+                       const msg_waypoint_note_t* p = msg_array[i].data;
+                       const char* s = p->name + p->name_size;
+                       unsigned nn = le_readu16(s);
+                       if (notes_max < notes_i + nn) {
+                               char* old = wp->notes;
+                               if (notes_max == 0) {
+                                       notes_max = nn;
+                               }
+                               do {
+                                       notes_max += notes_max;
+                               } while (notes_max < notes_i + nn);
+                               wp->notes = xmalloc(notes_max);
+                               if (old) {
+                                       memcpy(wp->notes, old, notes_i);
+                                       xfree(old);
+                               }
+                       }
+                       if (nn) {
+                               memcpy(wp->notes + notes_i, s + 2, nn);
+                               notes_i += nn;
+                               if (wp->notes[notes_i - 1] == 0) {
+                                       notes_i--;
+                               }
+                       }
+               } else {
+                       fatal(MYNAME ": unexpected message %x while reading waypoints\n", id);
+               }
+               message_free(&msg_array[i]);
+       }
+       xfree(msg_array);
+}
+
+//-----------------------------------------------------------------------------
+// Waypoint writing
+
+static void
+encode_time(time_t time_, gbuint8* p)
+{
+       const struct tm* t = gmtime(&time_);
+       p[0] = t->tm_year;
+       p[1] = t->tm_mon + 1;
+       p[2] = t->tm_mday;
+       p[3] = t->tm_hour;
+       p[4] = t->tm_min;
+       p[5] = t->tm_sec;
+}
+
+static void
+get_gc_notes(const waypoint* wp, int* symbol, char** notes, unsigned* notes_size)
+{
+       fs_xml* fs_gpx;
+       xml_tag* root = NULL;
+       gbfile* fd = gbfopen(NULL, "w", MYNAME);
+       const char* size = NULL;
+       int gc_sym = 0;
+
+       switch (wp->gc_data->type) {
+       case gt_traditional: gc_sym = 160; break;
+       case gt_multi: gc_sym = 161; break;
+       case gt_virtual: gc_sym = 169; break;
+       case gt_letterbox: gc_sym = 163; break;
+       case gt_event: gc_sym = 165; break;
+       case gt_suprise: gc_sym = 162; break;
+       case gt_webcam: gc_sym = 170; break;
+       case gt_earth: gc_sym = 168; break;
+       case gt_benchmark: gc_sym = 172; break;
+       case gt_cito: gc_sym = 167; break;
+       case gt_mega: gc_sym = 166; break;
+       case gt_unknown:
+       case gt_locationless:
+       case gt_ape:
+               break;
+       }
+       if (wp->description) {
+               gbfputs(wp->description, fd);
+               if (wp->gc_data->placer) {
+                       gbfprintf(fd, " by %s", wp->gc_data->placer);
+               }
+               gbfputc('\n', fd);
+       }
+       gbfprintf(fd, "Cache ID: %s\n", wp->shortname);
+       if (gc_sym) {
+               gbfprintf(fd, "%s\n", waypoint_symbol(gc_sym));
+               *symbol = gc_sym;
+       } else if (wp->icon_descr) {
+               gbfprintf(fd, "%s\n", wp->icon_descr);
+       }
+       switch (wp->gc_data->container) {
+       case gc_micro: size = "Micro"; break;
+       case gc_small: size = "Small"; break;
+       case gc_regular: size = "Regular"; break;
+       case gc_large: size = "Large"; break;
+       case gc_unknown:
+       case gc_other:
+       case gc_virtual:
+               break;
+       }
+       if (size) {
+               gbfprintf(fd, "SIZE: %s\n", size);
+       }
+       if (wp->gc_data->diff % 10) {
+               gbfprintf(fd, "D%.1f", wp->gc_data->diff / 10.0);
+       } else {
+               gbfprintf(fd, "D%u", wp->gc_data->diff / 10);
+       }
+       if (wp->gc_data->terr % 10) {
+               gbfprintf(fd, "/T%.1f\n", wp->gc_data->terr / 10.0);
+       } else {
+               gbfprintf(fd, "/T%u\n", wp->gc_data->terr / 10);
+       }
+       if (wp->gc_data->hint) {
+               gbfprintf(fd, "HINT: %s\n", wp->gc_data->hint);
+       }
+       if (wp->gc_data->desc_short.utfstring || wp->gc_data->desc_long.utfstring) {
+               gbfputs("DESC: ", fd);
+               if (wp->gc_data->desc_short.utfstring) {
+                       char* s1 = strip_html(&wp->gc_data->desc_short);
+                       char* s2 = cet_str_utf8_to_any(s1, global_opts.charset);
+                       gbfprintf(fd, "%s\n", s2);
+                       xfree(s2);
+                       xfree(s1);
+               }
+               if (wp->gc_data->desc_long.utfstring) {
+                       char* s1 = strip_html(&wp->gc_data->desc_long);
+                       char* s2 = cet_str_utf8_to_any(s1, global_opts.charset);
+                       gbfputs(s2, fd);
+                       xfree(s2);
+                       xfree(s1);
+               }
+       }
+       fs_gpx = (fs_xml*)fs_chain_find(wp->fs, FS_GPX);
+       if (opt_logs && fs_gpx && fs_gpx->tag) {
+               root = xml_findfirst(fs_gpx->tag, "groundspeak:logs");
+       }
+       if (root) {
+               xml_tag* curlog = xml_findfirst(root, "groundspeak:log");
+               if (curlog) {
+                       gbfputs("\nLOG:\n", fd);
+               }
+               for (; curlog; curlog = xml_findnext(root, curlog, "groundspeak:log")) {
+                       xml_tag* logpart = xml_findfirst(curlog, "groundspeak:type");
+                       if (logpart) {
+                               gbfprintf(fd, "%s\n", logpart->cdata);
+                       }
+                       logpart = xml_findfirst(curlog, "groundspeak:date");
+                       if (logpart) {
+                               time_t logtime = xml_parse_time(logpart->cdata, NULL);
+                               const struct tm* logtm = gmtime(&logtime);
+                               gbfprintf(fd, "%d-%d-%d ", logtm->tm_year + 1900, logtm->tm_mon + 1, logtm->tm_mday);
+                       }
+                       logpart = xml_findfirst(curlog, "groundspeak:finder");
+                       if (logpart) {
+                               char* s = cet_str_utf8_to_any(logpart->cdata, global_opts.charset);
+                               gbfputs(s, fd);
+                               xfree(s);
+                       }
+                       logpart = xml_findfirst(curlog, "groundspeak:text");
+                       if (logpart) {
+                               char* s = cet_str_utf8_to_any(logpart->cdata, global_opts.charset);
+                               gbfprintf(fd, ", %s", s);
+                               xfree(s);
+                       }
+                       gbfputc('\n', fd);
+               }
+       }
+       gbfputc(0, fd);
+       *notes_size = fd->memlen;
+       *notes = xmalloc(*notes_size);
+       memcpy(*notes, fd->handle.mem, *notes_size);
+       gbfclose(fd);
+}
+
+static void
+write_waypoint_notes(const char* notes, unsigned size, const char* name)
+{
+       message_t m;
+       const unsigned name_size = strlen(name) + 1;
+       const unsigned bytes_per_msg = (10 * (delbin_os_packet_size - 2)) - name_size - 20;
+       const unsigned msg_count = (size + (bytes_per_msg - 1)) / bytes_per_msg;
+       unsigned i = 1;
+
+       do {
+               char* pp;
+               unsigned n = bytes_per_msg;
+               msg_waypoint_note_t* p;
+               message_init_size(&m, 2 + 2 + 1 + name_size + 2 + bytes_per_msg);
+               p = m.data;
+               le_write16(p->index, i++);
+               le_write16(p->total, msg_count);
+               p->name_size = name_size;
+               memcpy(p->name, name, p->name_size);
+               pp = p->name + p->name_size;
+               if (n > size) {
+                       n = size;
+               }
+               le_write16(pp, n);
+               pp += 2;
+               memcpy(pp, notes, n);
+               pp += n;
+               if (*(pp - 1)) {
+                       *pp++ = 0;
+               }
+               notes += n;
+               size -= n;
+               m.size = pp - (char*)p;
+               add_to_batch(MSG_WAYPOINT_NOTE_IN, &m);
+       } while (size != 0);
+}
+
+static void
+write_waypoint(const waypoint* wp)
+{
+       message_t m;
+       msg_waypoint_t* p;
+       const char* name = wp->shortname;
+       unsigned name_size;
+       char* notes;
+       unsigned notes_size = 0;
+       unsigned extended_notes_size = 0;
+       char* notes_freeable = NULL;
+       int symbol = -1;
+       float elev = UNKNOWN_ELEV;
+       char* pp;
+
+       if (waypt_empty_gc_data(wp)) {
+               notes = wp->notes;
+               if (notes == NULL && wp->description && strcmp(wp->shortname, wp->description)) {
+                       notes = wp->description;
+               }
+               if (notes) {
+                       notes_size = strlen(notes) + 1;
+               }
+       } else {
+               get_gc_notes(wp, &symbol, &notes, &notes_size);
+               notes_freeable = notes;
+               if (wp->description) {
+                       name = wp->description;
+               }
+       }
+       if (notes_size > 800) {
+               if (use_extended_notes) {
+                       extended_notes_size = notes_size;
+                       notes_size = 1;
+               } else {
+                       notes_size = 800;
+               }
+       }
+       name_size = strlen(name) + 1;
+       if (name_size > 255) {
+               name_size = 255;
+       }
+       message_init_size(&m, 31 + name_size + notes_size);
+       p = m.data;
+
+       waypoint_i++;
+       le_write32(p->total, waypoint_n);
+       le_write32(p->index, waypoint_i);
+       encode_time(wp->creation_time, &p->year);
+       le_write32(p->latitude, delbin_deg2rad(wp->latitude));
+       le_write32(p->longitude, delbin_deg2rad(wp->longitude));
+       if (wp->altitude > unknown_alt) {
+               elev = wp->altitude;
+       }
+       le_write_float(p->elevation, elev);
+       if (symbol < 0) {
+               symbol = 0;
+               if (wp->icon_descr) {
+                       symbol = waypoint_symbol_index(wp->icon_descr);
+               }
+       }
+       p->symbol = symbol;
+       p->name_size = name_size;
+       memcpy(p->name, name, name_size - 1);
+       p->name[name_size - 1] = 0;
+       pp = p->name + name_size;
+       m.size = (pp + 2 + notes_size) - (char*)p;
+       if (extended_notes_size) {
+               le_write16(pp, 0xffff);
+               pp[2] = 0;
+       } else {
+               le_write16(pp, notes_size);
+               if (notes) {
+                       memcpy(pp + 2, notes, notes_size - 1);
+                       pp[2 + notes_size - 1] = 0;
+               }
+       }
+
+       add_to_batch(MSG_WAYPOINT_IN, &m);
+
+       if (extended_notes_size) {
+               write_waypoint_notes(notes, extended_notes_size, name);
+       }
+       if (notes_freeable) {
+               xfree(notes_freeable);
+       }
+       if (global_opts.debug_level >= DBGLVL_L)
+               warning(MYNAME ": wrote waypoint %u '%s'\n", waypoint_i, name);
+}
+
+static void
+write_waypoints(void)
+{
+       waypoint_i = 0;
+       waypoint_n = waypt_count();
+       waypt_disp_all(write_waypoint);
+       send_batch(TRUE);
+}
+
+//-----------------------------------------------------------------------------
+// Track reading
+
+static void
+decode_sat_fix(waypoint* wp, const gbuint8 status)
+{
+       switch (status & 3) {
+       case 1: wp->fix = fix_none; break;
+       case 2: wp->fix = fix_2d; break;
+       case 3:
+               wp->fix = fix_3d;
+               if (status & 4) {
+                       wp->fix = fix_dgps;
+               }
+               break;
+       }
+}
+
+static void
+decode_track_point(const void* data, unsigned* wp_array_i, unsigned max_point)
+{
+       const msg_track_point_t* p = data;
+       const unsigned n = p->number;
+       unsigned i;
+       unsigned j = *wp_array_i;
+
+       if (j + n > max_point) {
+               fatal(MYNAME ": read too many track points\n");
+       }
+       for (i = 0; i < n; i++, j++) {
+               waypoint* wp = waypt_new();
+               float elev = le_read_float(p->point[i].elevation);
+               wp_array[j] = wp;
+               wp->creation_time = decode_time(&p->point[i].year);
+               wp->latitude = delbin_rad2deg(le_read32(p->point[i].latitude));
+               wp->longitude = delbin_rad2deg(le_read32(p->point[i].longitude));
+               if (elev > UNKNOWN_ELEV) {
+                       wp->altitude = elev;
+               }
+               wp->speed = le_readu16(p->point[i].speed);
+               wp->speed *= (100.0f / (60 * 60));
+               wp->wpt_flags.speed = 1;
+               decode_sat_fix(wp, p->point[i].status);
+               // use microseconds as track segment marker
+               wp->microseconds = p->point[i].status & 0x10;
+       }
+       *wp_array_i = j;
+}
+
+static void
+read_track(route_head* track)
+{
+       message_t m;
+       message_t* msg_array;
+       const msg_track_header_t* p;
+       unsigned msg_array_n;
+       unsigned wp_array_i = 0;
+       unsigned n_point;
+       unsigned segment = 1;
+       char* track_name = NULL;
+       unsigned i;
+       int attempt = ATTEMPT_MAX;
+
+       message_init(&m);
+       // read track messages
+       for (;;) {
+               m.size = MSG_REQUEST_TRACKS_SIZE;
+               memset(m.data, 0, m.size);
+               ((char*)m.data)[0] = 1;  // Download single track
+               strcpy((char*)m.data + 1, track->rte_name);
+               message_write(MSG_REQUEST_TRACKS, &m);
+               if (get_batch(&msg_array, &msg_array_n))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading track '%s' failed\n", track->rte_name);
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": timed out reading track '%s', retrying\n", track->rte_name);
+               m.size = MSG_BREAK_SIZE;
+               memset(m.data, 0, m.size);
+               message_write(MSG_BREAK, &m);
+       }
+       message_free(&m);
+       if (msg_array_n == 0 || message_get_id(&msg_array[0]) != MSG_TRACK_HEADER_OUT) {
+               fatal(MYNAME ": reading track '%s' failed (missing track header)\n", track->rte_name);
+       }
+       // process track messages
+       p = msg_array[0].data;
+       if (le_readu16(p->comment_size)) {
+               track->rte_desc = xstrdup(p->comment);
+       }
+       track->line_color.bbggrr = track_color(p->color[0]);
+       n_point = le_readu32(p->total_points);
+       wp_array = xcalloc(n_point, sizeof(*wp_array));
+       message_free(&msg_array[0]);
+       for (i = 1; i < msg_array_n; i++) {
+               unsigned id = message_get_id(&msg_array[i]);
+               if (id == MSG_TRACK_POINT_OUT) {
+                       decode_track_point(msg_array[i].data, &wp_array_i, n_point);
+               } else {
+                       fatal(MYNAME ": unexpected message %x while reading track '%s'\n", id, track->rte_name);
+               }
+               message_free(&msg_array[i]);
+       }
+       xfree(msg_array);
+       if (n_point != wp_array_i) {
+               fatal(MYNAME ": track point count mismatch, expected %u, got %u\n", n_point, wp_array_i);
+       }
+       if (global_opts.debug_level >= DBGLVL_L)
+               warning(MYNAME ": read track '%s' %u points\n", track->rte_name, n_point);
+       // make track (one for each segment)
+       track_add_wpt(track, wp_array[0]);
+       for (i = 1; i < n_point; i++) {
+               // if track segment marker
+               if (wp_array[i]->microseconds) {
+                       const char* desc = track->rte_desc;
+                       if (track_name == NULL) {
+                               // save original name, append " #1" to first segment name
+                               track_name = track->rte_name;
+                               track->rte_name = xmalloc(strlen(track_name) + 4);
+                               sprintf(track->rte_name, "%s #1", track_name);
+                       }
+                       // make a new track for segment
+                       segment++;
+                       track_add_head(track);
+                       track = route_head_alloc();
+                       track->rte_name = xmalloc(strlen(track_name) + 7);
+                       sprintf(track->rte_name, "%s #%u", track_name, segment);
+                       track->rte_desc = xstrdup(desc);
+               }
+               wp_array[i]->microseconds = 0;
+               track_add_wpt(track, wp_array[i]);
+       }
+       track_add_head(track);
+       if (track_name) {
+               xfree(track_name);
+       }
+       xfree(wp_array);
+}
+
+static void
+read_tracks(void)
+{
+       message_t m;
+       message_t* msg_array;
+       unsigned msg_array_n;
+       route_head** track_array;
+       unsigned total;
+       unsigned i;
+       int attempt = ATTEMPT_MAX;
+
+       message_init(&m);
+       // get number of tracks
+       for (;;) {
+               m.size = 0;
+               message_write(MSG_TRACK_COUNT, &m);
+               if (message_read(MSG_TRACK_COUNT, &m))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading track count failed\n");
+       }
+       total = le_readu32(m.data);
+       if (global_opts.debug_level >= DBGLVL_L)
+               warning(MYNAME ": %u tracks\n", total);
+       if (total == 0) {
+               message_free(&m);
+               return;
+       }
+
+       // First get track headers, then request each track with non-zero number of points
+       attempt = ATTEMPT_MAX;
+       for (;;) {
+               m.size = MSG_REQUEST_TRACKS_SIZE;
+               memset(m.data, 0, m.size);
+               ((char*)m.data)[0] = 2;  // Download all track headers
+               message_write(MSG_REQUEST_TRACKS, &m);
+               if (get_batch(&msg_array, &msg_array_n))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading track headers failed\n");
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": timed out reading track headers, retrying\n");
+               m.size = MSG_BREAK_SIZE;
+               memset(m.data, 0, m.size);
+               message_write(MSG_BREAK, &m);
+       }
+       message_free(&m);
+       track_array = xcalloc(total, sizeof(*track_array));
+       for (i = 0; i < msg_array_n; i++) {
+               unsigned id = message_get_id(&msg_array[i]);
+               if (id == MSG_TRACK_HEADER_OUT) {
+                       const msg_track_header_t* p = msg_array[i].data;
+                       if (le_readu32(p->total_points)) {
+                               track_array[i] = route_head_alloc();
+                               track_array[i]->rte_name = xstrdup(p->name);
+                       }
+               } else {
+                       fatal(MYNAME ": unexpected message %x while reading track headers\n", id);
+               }
+               message_free(&msg_array[i]);
+       }
+       xfree(msg_array);
+       // get each track
+       for (i = 0; i < total; i++) {
+               if (track_array[i]) {
+                       read_track(track_array[i]);
+               }
+       }
+       xfree(track_array);
+}
+
+//-----------------------------------------------------------------------------
+// Track writing
+
+static void
+write_track_points(void)
+{
+       message_t m;
+       const unsigned pt_per_msg = 10;
+       msg_track_point_t* p = NULL;
+       unsigned i = 0;
+       unsigned j = 0;
+
+       do {
+               const waypoint* wp = wp_array[i];
+               float f;
+
+               if (j == 0) {
+                       message_init_size(&m, 9 + 23 * pt_per_msg);
+                       p = m.data;
+                       le_write32(p->total, waypoint_n);
+                       le_write32(p->index, i + 1);
+               }
+               assert(p);
+               encode_time(wp->creation_time, &p->point[j].year);
+               le_write32(p->point[j].latitude, delbin_deg2rad(wp->latitude));
+               le_write32(p->point[j].longitude, delbin_deg2rad(wp->longitude));
+               f = UNKNOWN_ELEV;
+               if (wp->altitude > unknown_alt) {
+                       f = wp->altitude;
+               }
+               le_write_float(p->point[j].elevation, f);
+               f = WAYPT_GET(wp, speed, 0);
+               f *= (60 * 60) / 100;
+               le_write16(p->point[j].speed, (gbuint16)f);
+               f = WAYPT_GET(wp, course, 0);
+               f *= 100;
+               le_write16(p->point[j].heading, (gbuint16)f);
+               switch (wp->fix) {
+               default:       p->point[j].status = 0; break;
+               case fix_none: p->point[j].status = 1; break;
+               case fix_2d:   p->point[j].status = 2; break;
+               case fix_3d:   p->point[j].status = 3; break;
+               case fix_dgps: p->point[j].status = 4 | 3; break;
+               }
+               i++;
+               j++;
+               if (j == pt_per_msg || i == waypoint_n) {
+                       p->number = j;
+                       m.size = 9 + 23 * j;
+                       add_to_batch(MSG_TRACK_POINT_IN, &m);
+                       j = 0;
+               }
+       } while (i < waypoint_n);
+}
+
+static void
+write_track_begin(const route_head* track)
+{
+       waypoint_i = 0;
+       waypoint_n = track->rte_waypt_ct;
+       if (waypoint_n) {
+               wp_array = xmalloc(waypoint_n * sizeof(*wp_array));
+       }
+}
+
+static void
+write_track_point(const waypoint* wp)
+{
+       wp_array[waypoint_i++] = (waypoint*)wp;
+}
+
+static void
+write_track_end(const route_head* track)
+{
+       message_t m;
+       msg_track_header_in_t* p;
+       unsigned comment_size = 0;
+
+       if (waypoint_n == 0) {
+               return;
+       }
+       if (track->rte_desc) {
+               comment_size = strlen(track->rte_desc) + 1;
+       }
+       message_init_size(&m, sizeof(msg_track_header_in_t) - 1 + comment_size);
+       p = m.data;
+       memset(p->name, 0, sizeof(p->name));
+       if (track->rte_name) {
+               strncpy(p->name, track->rte_name, sizeof(p->name) - 1);
+       } else {
+               sprintf(p->name, "%lu", (long)wp_array[0]->creation_time);
+       }
+       le_write32(p->total_points, waypoint_n);
+       encode_time(current_time(), &p->year);
+       le_write16(p->color, track_color_index(track->line_color.bbggrr));
+       le_write16(p->comment_size, comment_size);
+       if (comment_size) {
+               memcpy(p->comment, track->rte_desc, comment_size);
+       }
+       add_to_batch(MSG_TRACK_HEADER_IN, &m);
+       write_track_points();
+       send_batch(FALSE);
+       xfree(wp_array);
+}
+
+static void
+write_tracks(void)
+{
+       track_disp_all(write_track_begin, write_track_end, write_track_point);
+}
+
+//-----------------------------------------------------------------------------
+// Route reading
+
+static void
+decode_route_shape(const void* data, unsigned* wp_array_i)
+{
+       const msg_route_shape_t* p = data;
+       const unsigned n = p->number;
+       unsigned i;
+       unsigned j = *wp_array_i;
+
+       for (i = 0; i < n; i++, j++) {
+               char buf[32];
+               waypoint* wp = waypt_new();
+               wp_array[j] = wp;
+               wp->latitude = delbin_rad2deg(le_read32(p->point[i].latitude));
+               wp->longitude = delbin_rad2deg(le_read32(p->point[i].longitude));
+               sprintf(buf, "SHP%03u", j);
+               wp->shortname = xstrdup(buf);
+       }
+       *wp_array_i = j;
+}
+
+static waypoint*
+decode_route_point(const void* data)
+{
+       const msg_route_point_t* p = data;
+       const char* s = NULL;
+       gbfile* fd = gbfopen(NULL, "w", MYNAME);
+       waypoint* wp = waypt_new();
+       if (p->name[0]) {
+               wp->shortname = xstrdup(p->name);
+       }
+       // give these a higher priority than the shape points
+       wp->route_priority = 1;
+       wp->latitude = delbin_rad2deg(le_read32(p->latitude));
+       wp->longitude = delbin_rad2deg(le_read32(p->longitude));
+       switch (p->itinerary_type) {
+       case 1: s = "Start"; break;
+       case 2: s = "Stop"; break;
+       case 3: s = "Finish"; break;
+       case 4: s = "Via"; break;
+       case 5: s = "Via Hidden"; break;
+       case 6:
+               switch (p->turn_type) {
+               case 1: s = "Turn, Straight"; break;
+               case 2: s = "Turn, Right"; break;
+               case 3: s = "Turn, Bear Right"; break;
+               case 4: s = "Turn, Keep Right"; break;
+               case 5: s = "Turn, Left"; break;
+               case 6: s = "Turn, Bear Left"; break;
+               case 7: s = "Turn, Keep Left"; break;
+               case 8: s = "Turn, Reverse Direction"; break;
+               case 9: s = "Turn, Street Name Change"; break;
+               }
+               break;
+       }
+       if (s) {
+               gbfprintf(fd, "Type: %s", s);
+       }
+       if (p->exit_label_size && p->exit_label[0]) {
+               gbfprintf(fd, "\nExit: %s", p->exit_label);
+       }
+       s = p->exit_label + p->exit_label_size;
+       if (s[0] && s[1]) {
+               gbfprintf(fd, "\n%s", s + 1);
+       }
+       if (fd->memlen) {
+               gbfputc(0, fd);
+               wp->notes = xmalloc(fd->memlen);
+               memcpy(wp->notes, fd->handle.mem, fd->memlen);
+       }
+       gbfclose(fd);
+       return wp;
+}
+
+static void
+read_route(route_head* route)
+{
+       message_t m;
+       message_t* msg_array;
+       const msg_route_header_t* p;
+       unsigned msg_array_n;
+       unsigned wp_array_i = 0;
+       unsigned route_total, shape_total, total;
+       unsigned i;
+       int attempt = ATTEMPT_MAX;
+
+       message_init(&m);
+       for (;;) {
+               m.size = MSG_REQUEST_ROUTES_SIZE;
+               memset(m.data, 0, m.size);
+               ((char*)m.data)[0] = 1;  // Download single route
+               strcpy((char*)m.data + 1, route->rte_name);
+               message_write(MSG_REQUEST_ROUTES, &m);
+               if (get_batch(&msg_array, &msg_array_n))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading route '%s' failed (timed out)\n", route->rte_name);
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": timed out reading route route '%s', retrying\n", route->rte_name);
+               m.size = MSG_BREAK_SIZE;
+               memset(m.data, 0, m.size);
+               message_write(MSG_BREAK, &m);
+       }
+       message_free(&m);
+       if (msg_array_n == 0 || message_get_id(&msg_array[0]) != MSG_ROUTE_HEADER_OUT) {
+               fatal(MYNAME ": missing route header\n");
+       }
+       p = msg_array[0].data;
+       route_total = le_readu32(p->total_route_point);
+       shape_total = le_readu32(p->total_shape_point);
+       total = route_total + shape_total;
+       wp_array = xcalloc(total, sizeof(*wp_array));
+       if (global_opts.debug_level >= DBGLVL_L) {
+               warning(MYNAME ": route '%s' %u points, %u shape points\n",
+                       route->rte_name, route_total, shape_total);
+       }
+       message_free(&msg_array[0]);
+       for (i = 1; i < msg_array_n; i++) {
+               unsigned id = message_get_id(&msg_array[i]);
+               if (id == MSG_ROUTE_POINT_OUT) {
+                       wp_array[wp_array_i] = decode_route_point(msg_array[i].data);
+                       if (global_opts.debug_level >= DBGLVL_L)
+                               warning(MYNAME ": route point '%s'\n", wp_array[wp_array_i]->shortname);
+                       wp_array_i++;
+               } else if (id == MSG_ROUTE_SHAPE_OUT) {
+                       decode_route_shape(msg_array[i].data, &wp_array_i);
+               } else {
+                       fatal(MYNAME ": unexpected message %x while reading route '%s'\n", id, route->rte_name);
+               }
+               message_free(&msg_array[i]);
+       }
+       xfree(msg_array);
+       if (total != wp_array_i) {
+               fatal(MYNAME ": route point count mismatch, expected %u, got %u\n", total, wp_array_i);
+       }
+       for (i = 0; i < total; i++) {
+               route_add_wpt(route, wp_array[i]);
+       }
+       xfree(wp_array);
+       route_add_head(route);
+}
+
+static void
+read_routes(void)
+{
+       message_t m;
+       message_t* msg_array;
+       unsigned msg_array_n;
+       route_head** route_array;
+       unsigned total;
+       unsigned i;
+       int attempt = ATTEMPT_MAX;
+
+       message_init(&m);
+       // get number of routes
+       for (;;) {
+               m.size = 0;
+               message_write(MSG_ROUTE_COUNT, &m);
+               if (message_read(MSG_ROUTE_COUNT, &m))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading route count failed\n");
+       }
+       total = le_readu32(m.data);
+       if (global_opts.debug_level >= DBGLVL_L)
+               warning(MYNAME ": %u routes\n", total);
+       if (total == 0) {
+               message_free(&m);
+               return;
+       }
+
+       // First get route headers, then request each route
+       attempt = ATTEMPT_MAX;
+       for (;;) {
+               m.size = MSG_REQUEST_ROUTES_SIZE;
+               memset(m.data, 0, m.size);
+               ((char*)m.data)[0] = 2;  // Download all route headers
+               message_write(MSG_REQUEST_ROUTES, &m);
+               if (get_batch(&msg_array, &msg_array_n))
+                       break;
+               if (--attempt == 0)
+                       fatal(MYNAME ": reading route headers failed\n");
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": timed out reading route headers, retrying\n");
+               m.size = MSG_BREAK_SIZE;
+               memset(m.data, 0, m.size);
+               message_write(MSG_BREAK, &m);
+       }
+       message_free(&m);
+       route_array = xcalloc(total, sizeof(*route_array));
+       for (i = 0; i < msg_array_n; i++) {
+               unsigned id = message_get_id(&msg_array[i]);
+               if (id == MSG_ROUTE_HEADER_OUT) {
+                       route_array[i] = route_head_alloc();
+                       route_array[i]->rte_name = xstrdup(((msg_route_header_t*)msg_array[i].data)->name);
+               } else {
+                       fatal(MYNAME ": unexpected message %x while reading route headers\n", id);
+               }
+               message_free(&msg_array[i]);
+       }
+       xfree(msg_array);
+       // get each route
+       for (i = 0; i < total; i++) {
+               read_route(route_array[i]);
+       }
+       xfree(route_array);
+}
+
+//-----------------------------------------------------------------------------
+// Route writing
+
+static unsigned route_point_n;
+static unsigned shape_point_n;
+static unsigned* shape_point_counts;
+
+static void
+write_route_shape_points(waypoint** array, unsigned n)
+{
+       message_t m;
+       const unsigned pt_per_msg = 25;
+       msg_route_shape_t* p = NULL;
+       unsigned i = 0;
+       unsigned j = 0;
+
+       do {
+               if (j == 0) {
+                       message_init_size(&m, 10 + 8 * pt_per_msg);
+                       p = m.data;
+                       le_write32(p->total, n);
+                       le_write32(p->index, i + 1);
+                       p->reserved = 0;
+               }
+               assert(p);
+               le_write32(p->point[j].latitude, delbin_deg2rad(array[i]->latitude));
+               le_write32(p->point[j].longitude, delbin_deg2rad(array[i]->longitude));
+               i++;
+               j++;
+               if (j == pt_per_msg || i == n) {
+                       p->number = j;
+                       m.size = 10 + 8 * j;
+                       add_to_batch(MSG_ROUTE_SHAPE_IN, &m);
+                       j = 0;
+               }
+       } while (i < n);
+}
+
+static void
+write_route_points(void)
+{
+       unsigned route_point_i = 0;
+       unsigned i = 0;
+
+       while (i < waypoint_n) {
+               message_t m;
+               unsigned shape_n;
+               const waypoint* wp = wp_array[i];
+               msg_route_point_t* p;
+               char* s;
+
+               message_init_size(&m, sizeof(msg_route_point_t) + 1 + 1 + 4);
+               p = m.data;
+               memset(m.data, 0, m.size);
+               route_point_i++;
+               shape_n = shape_point_counts[route_point_i];
+               le_write32(p->total, route_point_n);
+               le_write32(p->index, route_point_i);
+               if (wp->shortname) {
+                       strncpy(p->name, wp->shortname, sizeof(p->name) - 1);
+               } else {
+                       sprintf(p->name, "RPT%u", route_point_i);
+               }
+               le_write32(p->latitude, delbin_deg2rad(wp->latitude));
+               le_write32(p->longitude, delbin_deg2rad(wp->longitude));
+               p->exit_label_size = 1;
+               s = p->exit_label + p->exit_label_size;
+               s[0] = 1;  // comment size
+               le_write32(s + 2, shape_n);
+               if (route_point_i == 1) {
+                       p->itinerary_type = 1; // start
+               } else if (route_point_i == route_point_n) {
+                       p->itinerary_type = 3; // finish
+               }
+               add_to_batch(MSG_ROUTE_POINT_IN, &m);
+               i++;
+               if (shape_n) {
+                       write_route_shape_points(&wp_array[i], shape_n);
+                       i += shape_n;
+               }
+       }
+}
+
+static void
+write_route_begin(const route_head* track)
+{
+       waypoint_i = 0;
+       route_point_n = 0;
+       shape_point_n = 0;
+       waypoint_n = track->rte_waypt_ct;
+       if (waypoint_n) {
+               wp_array = xmalloc(waypoint_n * sizeof(*wp_array));
+               shape_point_counts = xcalloc(waypoint_n, sizeof(*shape_point_counts));
+       }
+}
+
+static void
+write_route_point(const waypoint* wp)
+{
+       const char* s = wp->shortname;
+       wp_array[waypoint_i++] = (waypoint*)wp;
+       if (s && s[0] == 'S' && s[1] == 'H' && s[2] == 'P' && s[3] >= '0' && s[3] <= '9') {
+               shape_point_n++;
+               shape_point_counts[route_point_n]++;
+       } else {
+               route_point_n++;
+       }
+}
+
+static void
+write_route_end(const route_head* route)
+{
+       message_t m;
+       msg_route_header_in_t* p;
+
+       if (waypoint_n == 0) {
+               return;
+       }
+       message_init_size(&m, sizeof(msg_route_header_in_t));
+       p = m.data;
+       memset(p->name, 0, sizeof(p->name));
+       if (route->rte_name) {
+               strncpy(p->name, route->rte_name, sizeof(p->name) - 1);
+       } else {
+               sprintf(p->name, "%lu", (long)wp_array[0]->creation_time);
+       }
+       p->type = 0;
+       le_write32(p->total_route_point, route_point_n);
+       le_write32(p->total_shape_point, shape_point_n);
+       add_to_batch(MSG_ROUTE_HEADER_IN, &m);
+       write_route_points();
+       send_batch(TRUE);
+       if (wp_array) {
+               xfree(wp_array);
+               xfree(shape_point_counts);
+       }
+}
+
+static void
+write_routes(void)
+{
+       route_disp_all(write_route_begin, write_route_end, write_route_point);
+}
+
+//-----------------------------------------------------------------------------
+// Current position
+
+static waypoint*
+decode_navmsg(const void* data)
+{
+       waypoint* wp = waypt_new();
+       const msg_navigation_t* p = data;
+       struct tm t;
+
+       t.tm_year = le_readu16(p->year) - 1900;
+       t.tm_mon = p->month - 1;
+       t.tm_mday = p->day;
+       t.tm_hour = p->hour;
+       t.tm_min = p->minute;
+       t.tm_sec = p->second;
+       wp->creation_time = mkgmtime(&t);
+       wp->sat = p->satellites;
+       wp->latitude = le_read_double(p->latitude);
+       wp->longitude = le_read_double(p->longitude);
+       wp->altitude = le_read_double(p->elevation);
+       wp->speed = le_read_float(p->speed);
+       wp->speed *= (1000.0f / (60 * 60));
+       wp->wpt_flags.speed = 1;
+       wp->course = le_readu16(p->heading);
+       wp->course /= 100;
+       wp->wpt_flags.course = 1;
+       decode_sat_fix(wp, p->fix_status);
+       wp->shortname = xstrdup("Position");
+       return wp;
+}
+
+static waypoint*
+read_position(void)
+{
+       waypoint* wp;
+       message_t m;
+
+       message_init(&m);
+       message_read(MSG_NAVIGATION, &m);
+       wp = decode_navmsg(m.data);
+       if (wp->fix > fix_none &&
+               message_read_1(MSG_SATELLITE_INFO, &m) == MSG_SATELLITE_INFO)
+       {
+               const msg_satellite_t* p = m.data;
+               wp->hdop = le_readu16(p->hdop);
+               wp->hdop /= 100;
+               wp->vdop = le_readu16(p->vdop);
+               wp->vdop /= 100;
+               wp->pdop = le_readu16(p->pdop);
+               wp->pdop /= 100;
+       }
+       message_free(&m);
+       return wp;
+}
+
+//-----------------------------------------------------------------------------
+
+static void
+delbin_rw_init(const char *fname)
+{
+       message_t m;
+
+       delbin_os_ops.init(fname);
+
+       // Send a break to clear any state from a previous failure
+       message_init(&m);
+       m.size = MSG_BREAK_SIZE;
+       memset(m.data, 0, m.size);
+       message_write(MSG_BREAK, &m);
+       // get version info
+       m.size = 0;
+       message_write(MSG_VERSION, &m);
+       if (message_read(MSG_VERSION, &m)) {
+               const msg_version_t* p = m.data;
+               if (global_opts.debug_level >= DBGLVL_L)
+                       warning(MYNAME ": device %s %s\n", p->product, p->firmware);
+               if (opt_long_notes) {
+                       use_extended_notes = TRUE;
+               } else if (strstr(p->product, "PN-40")) {
+                       // Don't know if pre-2.5 PN-40 firmware or PN-20 can handle 0xb016 message
+                       use_extended_notes = p->firmware[0] > '2' ||
+                               (p->firmware[0] == '2' && p->firmware[2] >= '5');
+               }
+       }
+       message_free(&m);
+}
+
+static void
+delbin_rw_deinit(void)
+{
+       delbin_os_ops.deinit();
+}
+
+static void
+delbin_read(void)
+{
+       if (doing_wpts) {
+               if (opt_getposn) {
+                       waypt_add(read_position());
+               } else {
+                       read_waypoints();
+               }
+       }
+       if (doing_trks) {
+               read_tracks();
+       }
+       if (doing_rtes) {
+               read_routes();
+       }
+}
+
+static void
+delbin_write(void)
+{
+       if (doing_wpts) {
+               write_waypoints();
+       }
+       if (doing_trks) {
+               write_tracks();
+       }
+       if (doing_rtes) {
+               write_routes();
+       }
+}
+
+static waypoint*
+delbin_rd_position(posn_status* status)
+{
+       waypoint* wp = read_position();
+       if (wp == NULL) {
+               status->request_terminate = 1;
+       }
+       return wp;
+}
+
+ff_vecs_t delbin_vecs = {
+       ff_type_serial,
+       FF_CAP_RW_ALL,
+       delbin_rw_init, 
+       delbin_rw_init,
+       delbin_rw_deinit,       
+       delbin_rw_deinit,
+       delbin_read,
+       delbin_write,
+       NULL,
+       delbin_args,
+       CET_CHARSET_LATIN1, 1,
+       { delbin_rw_init, delbin_rd_position, delbin_rw_deinit }
+};
+
+//=============================================================================
+// OS device I/O implementations
+
+#define VENDOR_ID 0x1163
+#define PRODUCT_ID 0x2020
+
+//-----------------------------------------------------------------------------
+// Windows
+#if _WIN32
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#include <setupapi.h>
+#include <hidsdi.h>
+
+static HANDLE hid_handle;
+
+static void
+win_os_init(const char* fname)
+{
+       GUID hid_guid;
+       HDEVINFO dev_info;
+       SP_DEVICE_INTERFACE_DATA dev_int_data;
+       PHIDP_PREPARSED_DATA hid_ppd;
+       HIDP_CAPS hid_caps;
+       const char* busy = "";
+       unsigned i;
+
+       hid_handle = INVALID_HANDLE_VALUE;
+       HidD_GetHidGuid(&hid_guid);
+       dev_info = SetupDiGetClassDevs(&hid_guid, NULL, NULL, DIGCF_DEVICEINTERFACE | DIGCF_PRESENT);
+       if (dev_info == INVALID_HANDLE_VALUE) {
+               fatal(MYNAME ": SetupDiGetClassDevs failed %u\n", GetLastError());
+       }
+       dev_int_data.cbSize = sizeof(dev_int_data);
+       for (i = 0; SetupDiEnumDeviceInterfaces(dev_info, NULL, &hid_guid, i, &dev_int_data); i++) {
+               union {
+                       SP_DEVICE_INTERFACE_DETAIL_DATA detail_data;
+                       char buf[300];
+               } u;
+               u.detail_data.cbSize = sizeof(u.detail_data);
+               if (SetupDiGetDeviceInterfaceDetail(dev_info,
+                   &dev_int_data, &u.detail_data, sizeof(u.buf), NULL, NULL))
+               {
+                       HANDLE h = CreateFile(u.detail_data.DevicePath,
+                               FILE_READ_DATA | FILE_WRITE_DATA, 0, NULL, OPEN_EXISTING, 0, NULL);
+                       if (h != INVALID_HANDLE_VALUE) {
+                               HIDD_ATTRIBUTES hid_attr;
+                               hid_attr.Size = sizeof(hid_attr);
+                               if (HidD_GetAttributes(h, &hid_attr) &&
+                                   hid_attr.VendorID == VENDOR_ID && hid_attr.ProductID == PRODUCT_ID)
+                               {
+                                       hid_handle = h;
+                                       break;
+                               }
+                               CloseHandle(h);
+                       } else if (GetLastError() == ERROR_SHARING_VIOLATION &&
+                               strstr(u.detail_data.DevicePath, "1163") &&
+                               strstr(u.detail_data.DevicePath, "2020"))
+                       {
+                               busy = " (device busy?)";
+                       }
+               }
+       }
+       SetupDiDestroyDeviceInfoList(dev_info);
+       if (hid_handle == INVALID_HANDLE_VALUE) {
+               fatal(MYNAME ": no DeLorme PN found%s\n", busy);
+       }
+       if (!HidD_GetPreparsedData(hid_handle, &hid_ppd)) {
+               fatal(MYNAME ": HidD_GetPreparsedData failed %u\n", GetLastError());
+       }
+       if (!HidP_GetCaps(hid_ppd, &hid_caps)) {
+               fatal(MYNAME ": HidP_GetCaps failed %u\n", GetLastError());
+       }
+       // report length includes report id
+       delbin_os_packet_size = hid_caps.InputReportByteLength - 1;
+       HidD_FreePreparsedData(hid_ppd);
+}
+
+static void
+win_os_deinit(void)
+{
+       CloseHandle(hid_handle);
+}
+
+static unsigned
+win_os_packet_read(void* buf)
+{
+       unsigned n;
+       char buf1[257];
+       // first byte is report id
+       if (ReadFile(hid_handle, buf1, delbin_os_packet_size + 1, &n, NULL) == 0) {
+               fatal(MYNAME ": ReadFile failed %u\n", GetLastError());
+       }
+       if (n > 0) {
+               n--;
+       }
+       memcpy(buf, buf1 + 1, n);
+       return n;
+}
+
+static unsigned
+win_os_packet_write(const void* buf, unsigned size)
+{
+       unsigned n;
+       char buf1[257];
+       // first byte is report id
+       buf1[0] = 0;
+       memcpy(buf1 + 1, buf, size);
+       if (WriteFile(hid_handle, buf1, delbin_os_packet_size + 1, &n, NULL) == 0) {
+               fatal(MYNAME ": WriteFile failed %u\n", GetLastError());
+       }
+       if (n > size) {
+               n = size;
+       }
+       return n;
+}
+
+delbin_os_ops_t delbin_os_ops = {
+       win_os_init,
+       win_os_deinit,
+       win_os_packet_read,
+       win_os_packet_write
+};
+
+#endif // _WIN32
+
+//-----------------------------------------------------------------------------
+// libusb
+#if HAVE_LIBUSB
+
+#include <usb.h>
+
+static struct usb_device* usb_dev;
+static usb_dev_handle* usb_handle;
+static int endpoint_in;
+static int endpoint_out;
+
+static void
+libusb_os_init(const char* fname)
+{
+       struct usb_bus* bus;
+       const struct usb_endpoint_descriptor* endpoint_desc;
+
+       usb_init();
+       usb_find_busses();
+       usb_find_devices();
+       for (bus = usb_busses; usb_dev == NULL && bus; bus = bus->next) {
+               struct usb_device* d;
+               for (d = bus->devices; d; d = d->next) {
+                       if (d->descriptor.idVendor == VENDOR_ID && d->descriptor.idProduct == PRODUCT_ID) {
+                               usb_dev = d;
+                               break;
+                       }
+               }
+       }
+       if (usb_dev == NULL) {
+               fatal(MYNAME ": no DeLorme PN found\n");
+       }
+       usb_handle = usb_open(usb_dev);
+       if (usb_handle == NULL) {
+               fatal(MYNAME ": %s\n", usb_strerror());
+       }
+
+       // Device has 1 configuration, 1 interface, 2 interrupt endpoints
+       if (usb_claim_interface(usb_handle, 0) < 0) {
+#if LIBUSB_HAS_DETACH_KERNEL_DRIVER_NP
+               if (usb_detach_kernel_driver_np(usb_handle, 0) < 0) {
+                       warning(MYNAME ": %s\n", usb_strerror());
+               }
+               if (usb_claim_interface(usb_handle, 0) < 0)
+#endif
+               {
+                       const char* s = usb_strerror();
+                       usb_close(usb_handle);
+                       fatal(MYNAME ": %s\n", s);
+               }
+       }
+       endpoint_desc = usb_dev->config[0].interface[0].altsetting[0].endpoint;
+       delbin_os_packet_size = endpoint_desc[0].wMaxPacketSize;
+       endpoint_in = endpoint_desc[0].bEndpointAddress;
+       endpoint_out = endpoint_desc[1].bEndpointAddress;
+       if ((endpoint_in & USB_ENDPOINT_DIR_MASK) == USB_ENDPOINT_OUT) {
+               int t = endpoint_in;
+               endpoint_in = endpoint_out;
+               endpoint_out = t;
+       }
+}
+
+static void
+libusb_os_deinit(void)
+{
+       usb_release_interface(usb_handle, 0);
+       usb_close(usb_handle);
+}
+
+static unsigned
+libusb_os_packet_read(void* buf)
+{
+       int n = usb_interrupt_read(usb_handle, endpoint_in, buf, delbin_os_packet_size, 2000);
+       if (n < 0) {
+               fatal(MYNAME ": %s\n", usb_strerror());
+       }
+       return n;
+}
+
+static unsigned
+libusb_os_packet_write(const void* buf, unsigned size)
+{
+       int n = usb_interrupt_write(usb_handle, endpoint_out, (char*)buf, size, 2000);
+       if (n < 0) {
+               fatal(MYNAME ": %s\n", usb_strerror());
+       }
+       return n;
+}
+
+#if __linux
+static const delbin_os_ops_t libusb_os_ops =
+#else
+delbin_os_ops_t delbin_os_ops =
+#endif
+{
+       libusb_os_init,
+       libusb_os_deinit,
+       libusb_os_packet_read,
+       libusb_os_packet_write
+};
+
+#endif // HAVE_LIBUSB
+
+//-----------------------------------------------------------------------------
+// Linux
+#if __linux
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+#include <linux/types.h>
+#include <linux/hiddev.h>
+#include <linux/hidraw.h>
+
+static int fd_hidraw;
+static int fd_hiddev;
+
+static int linuxhid_os_init_status;
+
+static void
+linuxhid_os_init(const char* fname)
+{
+       struct hidraw_devinfo info;
+       struct hiddev_report_info rinfo;
+       struct hiddev_field_info finfo;
+       DIR* dir = NULL;
+       struct dirent* d;
+
+       fd_hidraw = fd_hiddev = -1;
+       if (fname && memcmp(fname, "hid:", 4) == 0) {
+               const char* raw_name = fname + 4;
+               const char* dev_name = strchr(raw_name, ',');
+               if (dev_name == NULL) {
+                       fatal(MYNAME ": missing hiddev\n");
+               }
+               fd_hidraw = open(raw_name, O_RDONLY);
+               if (fd_hidraw < 0) {
+                       fatal(MYNAME ": %s: %s\n", raw_name, strerror(errno));
+               }
+               dev_name++;
+               fd_hiddev = open(dev_name, O_WRONLY);
+               if (fd_hiddev < 0) {
+                       fatal(MYNAME ": %s: %s\n", dev_name, strerror(errno));
+               }
+       } else {
+               dir = opendir("/dev");
+       }
+       while (dir && (d = readdir(dir)) != NULL) {
+               if (strncmp(d->d_name, "hidraw", 6) == 0) {
+                       int fd1, fd2;
+                       char raw_name[32];
+                       char dev_name[32];
+                       sprintf(raw_name, "/dev/%s", d->d_name);
+                       fd1 = open(raw_name, O_RDONLY);
+                       if (fd1 < 0) {
+                               if (global_opts.debug_level >= DBGLVL_M)
+                                       warning(MYNAME ": %s: %s\n", raw_name, strerror(errno));
+                               continue;
+                       }
+                       sprintf(dev_name, "/dev/usb/hiddev%s", raw_name + sizeof("/dev/hidraw") - 1);
+                       fd2 = open(dev_name, O_WRONLY);
+                       if (fd2 < 0 && errno == ENOENT) {
+                               sprintf(dev_name, "/dev/hiddev%s", raw_name + sizeof("/dev/hidraw") - 1);
+                               fd2 = open(dev_name, O_WRONLY);
+                       }
+                       if (fd2 < 0) {
+                               if (global_opts.debug_level >= DBGLVL_M)
+                                       warning(MYNAME ": %s: %s\n", dev_name, strerror(errno));
+                               close(fd1);
+                               continue;
+                       }
+                       if (ioctl(fd1, HIDIOCGRAWINFO, &info) == 0 &&
+                           info.vendor == VENDOR_ID && info.product == PRODUCT_ID)
+                       {
+                               fd_hidraw = fd1;
+                               fd_hiddev = fd2;
+                               break;
+                       }
+                       close(fd1);
+                       close(fd2);
+               }
+       }
+       if (dir) {
+               closedir(dir);
+       }
+       if (fd_hidraw < 0) {
+               if (linuxhid_os_init_status == 0)
+                       fatal(MYNAME ": no DeLorme PN found\n");
+               return;
+       }
+       rinfo.report_type = HID_REPORT_TYPE_INPUT;
+       rinfo.report_id = HID_REPORT_ID_FIRST;
+       if (ioctl(fd_hiddev, HIDIOCGREPORTINFO, &rinfo) < 0) {
+               warning(MYNAME ": HIDIOCGREPORTINFO: %s\n", strerror(errno));
+               if (linuxhid_os_init_status == 0)
+                       exit(1);
+               return;
+       }
+       finfo.report_type = rinfo.report_type;
+       finfo.report_id = rinfo.report_id;
+       finfo.field_index = 0;
+       if (ioctl(fd_hiddev, HIDIOCGFIELDINFO, &finfo) < 0) {
+               warning(MYNAME ": HIDIOCGFIELDINFO: %s\n", strerror(errno));
+               if (linuxhid_os_init_status == 0)
+                       exit(1);
+               return;
+       }
+       delbin_os_packet_size = finfo.maxusage;
+       linuxhid_os_init_status = 0;
+}
+
+static void
+linuxhid_os_deinit(void)
+{
+       close(fd_hidraw);
+       close(fd_hiddev);
+}
+
+static unsigned
+linuxhid_os_packet_read(void* buf)
+{
+       int n = read(fd_hidraw, buf, delbin_os_packet_size);
+       if (n < 0) {
+               fatal(MYNAME ": %s\n", strerror(errno));
+       }
+       return n;
+}
+
+static unsigned
+linuxhid_os_packet_write(const void* buf, unsigned size)
+{
+       struct hiddev_usage_ref_multi urefm;
+       struct hiddev_report_info rinfo;
+       const gbuint8* p = buf;
+       unsigned i;
+
+       for (i = 0; i < size; i++) {
+               urefm.values[i] = p[i];
+       }
+       urefm.num_values = size;
+       memset(&urefm.uref, 0, sizeof(urefm.uref));
+       urefm.uref.report_type = HID_REPORT_TYPE_OUTPUT;
+       if (ioctl(fd_hiddev, HIDIOCSUSAGES, &urefm)) {
+               fatal(MYNAME ": HIDIOCSUSAGES: %s\n", strerror(errno));
+       }
+       memset(&rinfo, 0, sizeof(rinfo));
+       rinfo.report_type = HID_REPORT_TYPE_OUTPUT;
+       if (ioctl(fd_hiddev, HIDIOCSREPORT, &rinfo)) {
+               fatal(MYNAME ": HIDIOCSREPORT: %s\n", strerror(errno));
+       }
+       return size;
+}
+
+static const delbin_os_ops_t linuxhid_os_ops = {
+       linuxhid_os_init,
+       linuxhid_os_deinit,
+       linuxhid_os_packet_read,
+       linuxhid_os_packet_write
+};
+
+static void linux_os_init(const char* fname);
+
+delbin_os_ops_t delbin_os_ops = {
+       linux_os_init,
+       NULL,
+       NULL,
+       NULL
+};
+
+static void
+linux_os_init(const char* fname)
+{
+       // tell linuxhid_os_init not to exit
+       linuxhid_os_init_status = 1;
+       linuxhid_os_init(fname);
+       if (linuxhid_os_init_status == 0) {
+               delbin_os_ops = linuxhid_os_ops;
+       } else {
+#if HAVE_LIBUSB
+               if (global_opts.debug_level >= DBGLVL_M)
+                       warning(MYNAME ": HID init failed, falling back to libusb\n");
+               delbin_os_ops = libusb_os_ops;
+               delbin_os_ops.init(fname);
+#else
+               fatal(MYNAME ": no DeLorme PN found\n");
+#endif
+       }
+}
+
+#endif // __linux
+
+//-----------------------------------------------------------------------------
+// stubs
+#if !(_WIN32 || __linux || HAVE_LIBUSB)
+static void
+stub_os_init(const char* fname)
+{
+       fatal(MYNAME ": OS not supported\n");
+}
+static void
+stub_os_deinit(void)
+{
+}
+static unsigned
+stub_os_packet_read(void* buf)
+{
+       return 0;
+}
+static unsigned
+stub_os_packet_write(const void* buf, unsigned size)
+{
+       return 0;
+}
+delbin_os_ops_t delbin_os_ops = {
+       stub_os_init,
+       stub_os_deinit,
+       stub_os_packet_read,
+       stub_os_packet_write
+};
+#endif
+// end OS device I/O implementations section
+//=============================================================================
+
+static const int track_color_bgr[] = {
+       0x0000ff, // red
+       0x00ffff, // yellow
+       0x008000, // green
+       0xff0000, // blue
+       0x808080, // gray
+       0xffffff, // white
+       0,        // black
+       0xffff00, // cyan
+       0xff00ff, // magenta
+       0x00a5ff, // orange
+       0x82004b, // indigo
+       0xeea5ee  // violet
+};
+
+static int track_color(unsigned i)
+{
+       int bgr = -1;
+       if (i < sizeofarray(track_color_bgr)) {
+               bgr = track_color_bgr[i];
+       }
+       return bgr;
+}
+
+static unsigned track_color_index(int bgr)
+{
+       unsigned i = sizeofarray(track_color_bgr);
+       do {
+               i--;
+       } while (i != 0 && track_color_bgr[i] != bgr);
+       return i;
+}
+
+static const char* const waypoint_symbol_name[] = {
+       // 0
+       "Red Map Pin",
+       "Dark Red Map Pin",
+       "Yellow Map Pin",
+       "Dark Yellow Map Pin",
+       "Green Map Pin",
+       "Dark Green Map Pin",
+       "Turquoise Map Pin",
+       "Dark Turquoise Map Pin",
+       "Blue Map Pin",
+       "Dark Blue Map Pin",
+       // 10
+       "Gray Map Pin",
+       "Dark Gray Map Pin",
+       "Red Flag",
+       "Dark Red Flag",
+       "Yellow Flag",
+       "Dark Yellow Flag",
+       "Green Flag",
+       "Dark Green Flag",
+       "Turquoise Flag",
+       "Dark Turquoise Flag",
+       // 20
+       "Blue Flag",
+       "Dark Blue Flag",
+       "Gray Flag",
+       "Dark Gray Flag",
+       "Red Dot",
+       "Dark Red Dot",
+       "Yellow Dot",
+       "Dark Yellow Dot",
+       "Green Dot",
+       "Dark Green Dot",
+       // 30
+       "Turquoise Dot",
+       "Dark Turquoise Dot",
+       "Blue Dot",
+       "Dark Blue Dot",
+       "Gray Dot",
+       "Dark Gray Dot",
+       "Small Red Dot",
+       "Small Dark Red Dot",
+       "Small Yellow Dot",
+       "Small Dark Yellow Dot",
+       // 40
+       "Small Green Dot",
+       "Small Dark Green Dot",
+       "Small Turquoise Dot",
+       "Small Dark Turquoise Dot",
+       "Small Blue Dot",
+       "Small Dark Blue Dot",
+       "Small Gray Dot",
+       "Small Dark Gray Dot",
+       "Arrow Up",
+       "Arrow Down",
+       // 50
+       "Arrow Left",
+       "Arrow Right",
+       "Arrow Up Left",
+       "Arrow Up Right",
+       "Arrow Down Left",
+       "Arrow Dow Right",
+       "Green Star",
+       "Yellow Square",
+       "Red X",
+       "Turquoise Circle",
+       // 60
+       "Purple Triangle",
+       "American Flag",
+       "Stop",
+       "Parking",
+       "First Aid",
+       "Dining",
+       "Railroad Crossing",
+       "Heliport",
+       "Restroom",
+       "Information",
+       // 70
+       "Diver Down",
+       "Exit",
+       "Health Facility",
+       "Police",
+       "Post Office",
+       "Mining",
+       "Danger",
+       "Money",
+       "Exclamation",
+       "Car",
+       // 80
+       "Jeep",
+       "Truck",
+       "Tow Truck",
+       "Motor Home",
+       "School Bus",
+       "Four-wheeler",
+       "Snowmobile",
+       "Sailboat",
+       "Powerboat",
+       "Boat Launch",
+       // 90
+       "Anchor",
+       "Buoy",
+       "Shipwreck",
+       "Glider Area",
+       "Private Airport",
+       "Public Airport",
+       "Military Airport",
+       "Military Base",
+       "House",
+       "Church",
+       // 100
+       "Building",
+       "School",
+       "Lighthouse",
+       "Bridge",
+       "Radio Tower",
+       "Dam",
+       "Tunnel",
+       "Toll Booth",
+       "Gas Station",
+       "Lodging",
+       // 110
+       "Telephone",
+       "Traffic Light",
+       "Fire Hydrant",
+       "Tombstone",
+       "Picnic Table",
+       "Tent",
+       "Shelter",
+       "Camper",
+       "Fire",
+       "Shower",
+       // 120
+       "Drinking Water",
+       "Binoculars",
+       "Camera",
+       "Geocache",
+       "Geocache Found",
+       "Fishing Pole",
+       "Ice Fishing Trap Set",
+       "Ice Fishing Trap Up",
+       "Moose",
+       "Deer",
+       // 130
+       "Bear",
+       "Bird",
+       "Duck",
+       "Fish",
+       "Deer Tracks",
+       "Animal Tracks",
+       "Bird Tracks",
+       "Birch Tree",
+       "Evergreen Tree",
+       "Deciduous Tree",
+       // 140
+       "Flower Garden",
+       "Mountain",
+       "Cave",
+       "Beach",
+       "Hiking",
+       "Swimming",
+       "Bicycling",
+       "Kayaking",
+       "Canoeing",
+       "Water Skiing",
+       // 150
+       "Cross-country Skiing",
+       "Downhill Skiing",
+       "Ice Skating",
+       "Dogsledding",
+       "Shooting",
+       "Golf Course",
+       "Ballpark",
+       // 157-182 added in PN-40 2.5 firmware
+       "Cache Found",
+       "Didn't Find It",
+       "My Cache",
+       // 160
+       "Traditional Cache",
+       "Multi-Cache",
+       "Unknown Cache",
+       "Letterbox Hybrid",
+       "Whereigo Cache",
+       "Event Cache",
+       "Mega-Event Cache",
+       "Cache In Trash Out Event",
+       "EarthCache",
+       "Virtual Cache",
+       // 170
+       "Webcam Cache",
+       "Waymark",
+       "NGS Benchmark",
+       "Write Note",
+       "Needs Maintenance",
+       "Final Location",
+       "Parking Area",
+       "Question to Answer",
+       "Reference Point",
+       "Stages of a Multicache",
+       // 180
+       "Trailhead",
+       "Temporarily Disable Listing",
+       "Enable Listing"
+};
+
+static const char*
+waypoint_symbol(unsigned i)
+{
+       const char* p = NULL;
+       if (i < sizeofarray(waypoint_symbol_name)) {
+               p = waypoint_symbol_name[i];
+       }
+       return p;
+}
+
+static unsigned
+waypoint_symbol_index(const char* name)
+{
+       static unsigned last_result;
+       static char last_name[32];
+       unsigned i = last_result;
+
+       if (strncmp(name, last_name, sizeof(last_name)) != 0) {
+               i = sizeofarray(waypoint_symbol_name);
+               do {
+                       i--;
+               } while (i != 0 && case_ignore_strcmp(name, waypoint_symbol_name[i]) != 0);
+               strncpy(last_name, name, sizeof(last_name));
+               last_result = i;
+       }
+       return i;
+}
+
+// vi: ts=4 sw=4 noexpandtab
diff --git a/vecs.c b/vecs.c
index 8ba8c0bbb1ed61001c5fbe21e510215f347cc8d6..7d369e1bf316cfea7378d6890c159aee05336823 100644 (file)
--- a/vecs.c
+++ b/vecs.c
@@ -45,6 +45,7 @@ extern ff_vecs_t compegps_vecs;
 extern ff_vecs_t copilot_vecs;
 extern ff_vecs_t coto_vecs;
 extern ff_vecs_t cst_vecs;
+extern ff_vecs_t delbin_vecs;
 extern ff_vecs_t dg100_vecs;
 extern ff_vecs_t easygps_vecs;
 extern ff_vecs_t garmin_vecs;
@@ -926,6 +927,12 @@ vecs_t vec_list[] = {
                "Naviguide binary route file (.twl)",
                "twl"
         },
+       {
+               &delbin_vecs, 
+               "delbin",
+               "DeLorme PN-20/PN-30/PN-40 USB protocol",
+               NULL
+       }, 
 
 #endif // MAXIMAL_ENABLED
        {